Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing AssetBalance type #5290

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft

Introducing AssetBalance type #5290

wants to merge 24 commits into from

Conversation

Janislav
Copy link
Contributor

@Janislav Janislav commented Sep 24, 2024

Pull Request

Closes: PRO-1466

Checklist

Please conduct a thorough self-review before opening the PR.

  • I am confident that the code works.
  • I have written sufficient tests.
  • I have written and tested required migrations.
  • I have updated documentation where appropriate.

Summary

Please include a succinct description of the purpose and content of the PR. What problem does it solve, and how? Link issues, discussions, other PRs, and anything else that will help the reviewer.

Non-Breaking changes

If this PR includes non-breaking changes, select the non-breaking label. On merge, CI will automatically cherry-pick the commit to a PR against the release branch.

Copy link

codecov bot commented Sep 24, 2024

Codecov Report

Attention: Patch coverage is 87.46439% with 44 lines in your changes missing coverage. Please review.

Project coverage is 70%. Comparing base (e7c9706) to head (2f52dce).

Files with missing lines Patch % Lines
state-chain/primitives/src/accounting.rs 86% 18 Missing ⚠️
state-chain/pallets/cf-asset-balances/src/lib.rs 83% 12 Missing and 2 partials ⚠️
state-chain/runtime/src/lib.rs 0% 0 Missing and 7 partials ⚠️
api/lib/src/queries.rs 0% 5 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff           @@
##            main   #5290    +/-   ##
======================================
- Coverage     70%     70%    -0%     
======================================
  Files        487     488     +1     
  Lines      87463   87568   +105     
  Branches   87463   87568   +105     
======================================
+ Hits       61511   61519     +8     
- Misses     22672   22758    +86     
- Partials    3280    3291    +11     
Flag Coverage Δ
70% <87%> (-<1%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Janislav Janislav marked this pull request as ready for review September 27, 2024 09:03
Copy link
Collaborator

@dandanlen dandanlen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some issues with this.

I want to have a go at refactoring it, I have some ideas on how to split up the types/traits etc a bit more clearly.

Will put the PR in draft mode for now.

pub struct AssetBalance {
/// The asset of the balance. For example, DOT, ETH, etc.
asset: Asset,
/// The amount of the balance.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: some of these doc comments don't add much information.

/// replacement for the AssetAmount type where it is possible and straight forward. It's intended
/// that this type is not deriving Copy or Clone to force the user to think about the handling of
/// the resource.
#[derive(Debug, Encode, Decode, TypeInfo, Eq)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to must_use?

Comment on lines +76 to +88
/// Subtracts the given amount from the balance, saturating at 0.
/// Note: This is a primitive operation and should be used with caution.
/// It is the caller's responsibility to ensure **not** to mix assets.
pub fn saturating_sub_amount(&mut self, amount: AssetAmount) {
self.amount = self.amount.saturating_sub(amount);
}

/// Adds the given amount to the balance, saturating at MAX.
/// Note: This is a primitive operation and should be used with caution.
/// It is the caller's responsibility to ensure **not** to mix assets.
pub fn saturating_add_amount(&mut self, amount: AssetAmount) {
self.amount = self.amount.saturating_add(amount);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods should not exist. If we allow the checks to be trivially avoided like this, then this whole change is a bit pointless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also had methods like this in your first draft, so I was thinking this can be useful or should be possible. But yeah, I was also wondering...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope the first draft didn't have these. The reason was that we also want to try to track ownership (this why we don't implement Clone or Copy).

Comment on lines +125 to +143
fn lt(&self, other: &Self) -> bool {
Self::ensure_asset_compatibility(self, other);
self.amount < other.amount
}

fn le(&self, other: &Self) -> bool {
Self::ensure_asset_compatibility(self, other);
self.amount <= other.amount
}

fn gt(&self, other: &Self) -> bool {
Self::ensure_asset_compatibility(self, other);
self.amount > other.amount
}

fn ge(&self, other: &Self) -> bool {
Self::ensure_asset_compatibility(self, other);
self.amount >= other.amount
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't need to be derived, they are provided through partial_cmp

@@ -140,12 +137,12 @@ pub mod pallet {
/// Liabilities are funds that are owed to some external party.
#[pallet::storage]
pub type Liabilities<T: Config> =
StorageMap<_, Twox64Concat, Asset, BTreeMap<ExternalOwner, AssetAmount>, ValueQuery>;
StorageMap<_, Twox64Concat, Asset, BTreeMap<ExternalOwner, AssetBalance>, OptionQuery>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't actually own these assets, I don't think use AssetBalance is correct here - should be AssetAmount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean by "own"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that the idea is that AssetBalance represents some assets that owned by network: controlled by the validators. Liabilities is for tracking things that are owed by the network: fees we have paid and need to refund, for example.

@@ -208,24 +205,24 @@ impl<T: Config> Pallet<T> {
fn reconcile(
chain: ForeignChain,
owner: &ExternalOwner,
amount_owed: &mut AssetAmount,
available: &mut AssetAmount,
amount_owed: &mut AssetBalance,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too - the amount owed is not some amount of asset that is owned on-chain, it's amount that is owed. I think using AssetAmount here would make more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense in a way that everywhere you use AssetBalance you have more guarantees, and it's saver to use also if you change the implementation in the future. If you assume that the implementation is 100% correct, and we will never do any changes again that could introduce new bugs mixing up assets or accounting assets in a wrong way, then there is not much point in changing it at all in my opinion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree checking the Asset might make sense here, but this is a rare case where we're comparing assets (owned) to liabilities (not owned). Most of the time we are dealing exclusively with assets owned by the network.


/// Ensures we consume the other asset, checks the compatibility and reduces it from asset
/// balance saturating at 0.
pub fn saturating_reduce(&mut self, other: Self) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also doesn't make much sense: we effectively delete other, which is what we're trying to avoid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is exactly what we want, or? We have 2 assets, we add the other asset to self, so other doesn't exist anymore (burned/destroyed/what ever). The whole point of this is to use the type system of rust to enforce handling an
value of an AssetBalance type in a way of a physical thing that is gone after I have used it. Like a coin for ex. I am a bit confused now...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this function signature would make sense for adding, but not reducing. How can can you reduce an amount by consuming some other amount? You can't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can. Lets say you have two Assets. A (self) is 50 B (other) is 30. You reduce A by B. B is consumed by saturating_reduce and A is 20. B is consumed and gone. Makes total sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did B go in this example?

It's like saying: I have €10 and you give me €2, I reduce my 10 by 2 and now I have €8.

/// Ensures we consume the other asset, checks the compatibility and adds it to asset balance.
/// Wraps the actual checked_add method and so provides the same functionality. Doesn't modify
/// the original balance.
pub fn checked_add(&self, other: Self) -> Option<Self> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't take ownership of self, so it duplicates funds: after calling this, the caller still owns self but now also has the result, which is self+other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, the doc comment is not correct here. I changed the implementation after writing this. The intense was to have exactly something like checked_add (which is useful) but with the check of the assets.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were 2 things we wanted to achieve: 1. avoid mixing assets. 2. avoid duplicating funds. This method duplicates funds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I see what you mean... It's true that checked_add doesn't make a lost of sense here

/// Ensures we consume the other asset, checks the compatibility and subtracts it from asset
/// balance. Wraps the actual checked_sub method and so provides the same functionality. Doesn't
/// modify the original balance.
pub fn checked_sub(&self, other: Self) -> Option<Self> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

}
}

impl PartialOrd for AssetBalance {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think deriving these is fine. (same for Ord and PartialEq).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact maybe it makes sense to not implement Eq/PartialEq (or only PartialEq?)... We might want to compare asset, or amount, but one balance is only equal to another balance if they are then same and we are ensuring that we can't duplicate balances.

In other words: if I have 1€ and you have 1€, we have the same amounts, and the same currency, but they are not the same coins. So asking "is my coin equal to yours?" is kind of meaningless.

Also Ord: it doesn't make sense to compare $1 to 1FLIP, so PartialOrd might be sufficient (which can return an ordering of None).

@dandanlen dandanlen marked this pull request as draft October 2, 2024 08:51
@Janislav
Copy link
Contributor Author

Janislav commented Oct 2, 2024

My 2 cents: So after reading your comments, I am not sure if this change really makes a lot of sense. The only real benefit we get is that we are more safe in the future with the guarantee a new type can offer (don't mix assets, don't account in a wrong way). It will not add more benefit to our current codebase (assuming it is correct) other than making the code more complex because it's more complex to handle this type and possibly adding new bugs because we have to do a lot of refracting. Most likely we will end up in a scenario where we use in some places AssetBalance and in some other AssetAmount and after some time no one will remember why we use type A here and type B there and everything is getting even more confusing.

@dandanlen
Copy link
Collaborator

You're entitled to your opinion - I think the change makes sense if we can figure out how to implement it properly and with minimal friction. We have had bugs in the past that would have been caught by a system like this.

assuming it is correct

Exactly - we have to just 'assume' that everywhere we deal with asset balances, it's always correct. Instead, if we make this change, we only have to assume that the AssetBalances type is implemented correcty. That's a much smaller assumption :)

I agree it's not an urgent priority though - I'll keep working on it in the background.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants